﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sharebert.OverlayLibrary.API;
using Sharebert.LinkLayerLibrary;
using Sharebert.LinkLayerLibrary.API;
using System.Threading;
using System.Security.Cryptography;
using Sharebert.LoggingLibrary.API;

namespace Sharebert.OverlayLibrary
{
    public class Overlay : IOverlay
    {
        enum State
        {
            /// <summary>
            /// Overlay is inactive (waiting to be used)
            /// </summary>
            Inactive,

            /// <summary>
            /// Connecting to the overlay
            /// </summary>
            Connecting,

            /// <summary>
            /// Successfully connected
            /// </summary>
            Connected,
            
            /// <summary>
            /// Shuting down the overlay
            /// </summary>
            Shutdown
        }

        private const int DEFAULT_BOOTSTRAP_PING_INTERVAL = 30;

        private const int DEFAULT_PEER_PING_INTERVAL = 15;

        private const int DEFAULT_TIMEOUT = 30;

        private const int DEFAULT_MIN_CONNECTION = 4;

        private const int DEFAULT_MAX_CONNECTION = 10;

        private const int DEFAULT_MAX_HOP_COUNT = 5;


        #region Constructor

        /// <summary>
        /// Initializes a new instance of the overlay. Selects a random port greater than 1023 to listen for new peer connections.
        /// </summary>
        public Overlay(string bsServerAddress, int bsServerPort, string listenAddress, ILogger logger)
        {
            Initialize(new BootstrapClient(bsServerAddress, bsServerPort, listenAddress, logger), logger);
        }

        /// <summary>
        /// Initializes a new instance of the overlay. Uses the specified port to listen for new peer connections.
        /// </summary>
        public Overlay(string bsServerAddress, int bsServerPort, string listenAddress, int listenPort, ILogger logger)
        {
            Initialize(new BootstrapClient(bsServerAddress, bsServerPort, listenAddress, listenPort, logger), logger);
        }

        private void Initialize(IBootstrapClient bsClient, ILogger logger)
        {
            this.Logger = logger;
            this.bsClient = bsClient;
            this.bsClient.ConnectionLost += new EventHandler<EventArgs>(HandleBootstrapTimedOut);
            this.bsClient.PeerListReceived += new EventHandler<PeerListReceivedEventArgs>(HandlePeerListReceived);
            this.PeerPingInverval = DEFAULT_PEER_PING_INTERVAL;
            this.BootstrapPingInverval = DEFAULT_BOOTSTRAP_PING_INTERVAL;
            this.ListenAddress = this.bsClient.ListenIP;
            this.ListenPort = this.bsClient.ListenPort;
            this.Timeout = DEFAULT_TIMEOUT;
            this.MinConnection = DEFAULT_MIN_CONNECTION;
            this.MaxConnection = DEFAULT_MAX_CONNECTION;
            this.MaxHopCount = DEFAULT_MAX_HOP_COUNT;
            this.Neighbours = new ThreadSafeList<IConnection>();
            this.listenConnection = new Connection();
            this.bootstrapPingTimer = new System.Timers.Timer();
            this.bootstrapPingTimer.Elapsed += new System.Timers.ElapsedEventHandler(HandleBootstrapPeerTimerElapsed);
            this.peerPingTimer = new System.Timers.Timer();
            this.peerPingTimer.Elapsed += new System.Timers.ElapsedEventHandler(HandlePeerPingTimerElapsed);
            this.listenThread = new Thread(new ParameterizedThreadStart(StartListen));
            this.listenThread.IsBackground = true;
            this.overlayState = State.Inactive;
        }

        #endregion


        #region Public Interface

        /// <summary>
        /// Connects to the p2p network.
        /// Starts bootstrapping to get some peers.
        /// </summary>
        public void Connect()
        {
            switch (this.overlayState)
            {
                case State.Inactive:
                case State.Shutdown:
                    this.overlayState = State.Connecting;
                    if (!this.listenThread.IsAlive)
                    {
                        this.listenThread.Start(this.listenConnection);
                    }
                    CheckRequestPeerList();
                    break;
                default:
                    this.Logger.Debug(String.Format("Already connected | Active connections: {0}", this.Neighbours.Count));
                    break;
            }
        }

        /// <summary>
        /// Perform a clean shutdown, which informs all connected peers.
        /// </summary>
        public void Offline()
        {
            if (this.overlayState == State.Inactive || this.overlayState == State.Shutdown)
            {
                return;
            }
            this.listenConnection.Close();

            try
            {
                this.bsClient.Offline();
            }
            catch (NetworkException ex)
            {
                this.Logger.Warn("Could not inform bootstrap server that we are are going offline.", ex);
            }

            byte[] payload = new byte[0];
            IHeader header = new Header(MessageTypes.OFFLINE, payload.Length);
            IMessage message = new Message(header, payload);
            foreach (IConnection conn in this.Neighbours)
            {
                try
                {
                    if (conn.Connected)
                    {
                        // inform client that we are going offline
                        conn.SendMessage(message);
                        CloseConnection(conn);
                    }
                    else
                    {
                        this.Logger.Warn("Invalid entry in neighbour list: " + conn.ToString());
                    }
                }
                catch (NetworkException ex)
                {
                    this.Logger.Warn("Could not send offline message to: " + conn.ToString(), ex);
                }
            }
            this.Neighbours.Clear();
            this.overlayState = State.Shutdown;
        }

        /// <summary>
        /// Perform a file search. This will flood the search request to all connected peers.
        /// </summary>
        /// <param name="regEx">The file regex to search for</param>
        public void Search(string regEx)
        {
            //generating search request message
            var hash = CalculateHash(regEx, DateTime.Now.ToString(), ListenAddress, ListenPort.ToString());
            //remember our search requests
            _WeSearchFor.Add(hash);
            var searchRequestMessage = new SearchRequestMessage(regEx, hash, 5);
            var payload = Serialisation.Serialize(searchRequestMessage, typeof (SearchRequestMessage));
            IHeader header = new Header(MessageTypes.SEARCH_REQUEST, payload.Length);
            IMessage message = new Message(header, payload);

            //flood search request to all neighbours
            foreach(var connection in Neighbours)
            {
                try
                {
                    connection.SendMessage(message);
                    this.Logger.Debug(String.Format("Send request message for {0} to {1}:{2}", regEx, connection.RemoteAddress, connection.RemotePort));
                }
                catch(Exception ex)
                {
                    this.Logger.Error(String.Format("Could not send request message for {0} to {1}:{2}: {3}", regEx, connection.RemoteAddress, connection.RemotePort,ex.Message));
                }
            }

        }

        /// <summary>
        /// Send a response with files fitting to regex to all neighbours
        /// </summary>
        /// <param name="files"></param>
        /// <param name="regEx"></param>
        /// <param name="hash"></param>
        /// <param name="downloadIP"></param>
        /// <param name="downloadPort"></param>
        public void Respond(FileOffer[] files, string regEx, byte[] hash,string downloadIP, int downloadPort)
        {
            var searchResponseMessage = new SearchResponseMessage(regEx, hash, 5, files, downloadIP, downloadPort);
            var payload = Serialisation.Serialize(searchResponseMessage, typeof(SearchResponseMessage));
            IHeader header = new Header(MessageTypes.SEARCH_RESPONSE, payload.Length);
            IMessage message = new Message(header, payload);

            //flood search response to all neighbours
            foreach (var connection in Neighbours)
            {
                try
                {
                    connection.SendMessage(message);
                    this.Logger.Debug(String.Format("Send response message for {0} to {1}:{2}", regEx, connection.RemoteAddress, connection.RemotePort));
                }
                catch (Exception ex)
                {
                    this.Logger.Error(String.Format("Could not send response message for {0} to {1}:{2}: {3}", regEx, connection.RemoteAddress, connection.RemotePort, ex.Message));
                }
            }
        }

        #endregion


        #region Handle messages

        private void HandleMessageReceived(object sender, MessageReceivedEventArgs e)
        {
            CleanLists();       // clean our memory from too old stuff

            var conn = (IConnection)sender;
            if (e.Message.Header.Command.Equals(MessageTypes.OFFLINE))
            {
                // Client is going offline, so we close the connection (Timer will get new connections)
                CloseConnection(conn);
                Neighbours.Remove(conn);
            }
            else if (e.Message.Header.Command.Equals(MessageTypes.SEARCH_REQUEST))
            {
                // Another peer is searching for files
                var request = (SearchRequestMessage)Serialisation.Deserialize(e.Message.Payload, typeof(SearchRequestMessage));

                //if we know the uid we already saw the message, so we do not have to do anything
                if (_SeenMessages.Contains(request.Uid))
                {
                    return;
                }
                _SeenMessages.Add(request.Uid);
                request.Hopcount += -1; //increase the hopcount by one

                //if the hopcount > 0 we continue flooding
                if(request.Hopcount > 0)
                {
                    //flood to everyone except the one who sent us the message
                    var connections = from Connection c in Neighbours
                                 where !c.RemoteAddress.Equals(conn.RemoteAddress) ||
                                       !c.RemotePort.Equals(conn.RemotePort)
                                 select c;

                    var payload = Serialisation.Serialize(request, typeof(SearchRequestMessage));
                    IHeader header = new Header(MessageTypes.SEARCH_REQUEST, payload.Length);
                    IMessage message = new Message(header, payload);

                    foreach (var con in connections)
                    {
                        try
                        {
                            con.SendMessage(message);
                            this.Logger.Debug(String.Format("Forward request message for {0} to {1}:{2}", request.Regex, con.RemoteAddress, con.RemotePort));
                        }
                        catch (Exception ex)
                        {
                            this.Logger.Error(String.Format("Could not forward request message for {0} to {1}:{2}: {3}", request.Regex, con.RemoteAddress, con.RemotePort, ex.Message));
                        }
                    }
                }

                var query = from SearchRequestMessage srm in (ArrayList)_AlreadySeenSearchRequests.Clone()
                            where EqualArrays(srm.Hash, request.Hash)
                            select srm;

                var query2 = from byte[] hash in (ArrayList)_WeSearchFor.Clone()
                            where EqualArrays(hash, request.Hash)
                            select hash;

                //we already saw this search request or its our request so we do not have to inform upper layer
                if(query.Count() > 0 || query2.Count() > 0)
                {
                    return;
                }

                //we do not know this request before so we store the message
                _AlreadySeenSearchRequests.Add(request);

                ProcessSearchRequest(this, new SearchRequestReceivedEventArgs(request.Regex,request.Hash));
            }
            else if (e.Message.Header.Command.Equals(MessageTypes.SEARCH_RESPONSE))
            {
                // A peer is responding to a search request
                var response = (SearchResponseMessage)Serialisation.Deserialize(e.Message.Payload, typeof(SearchResponseMessage));

                //if we know the uid we already saw the message, so we do not have to do anything
                if (_SeenMessages.Contains(response.Uid))
                {
                    return;
                }
                _SeenMessages.Add(response.Uid);
                response.Hopcount += -1; //increase the hopcount by one

                //check if this is for us
                var query = from byte[] hash in (ArrayList)_WeSearchFor.Clone()
                            where EqualArrays(hash, response.Hash)
                            select hash;
                
                //this is an answer to our request so we handle it
                if(query.Count() > 0)
                {
                    this.Logger.Debug(String.Format("Got respond to request {0} from {1}:{2}", response.Regex, conn.RemoteAddress,conn.RemotePort));
                    var results = new SearchResult[1];
                    results[0] = new SearchResult(conn.RemoteAddress,response.DownloadPort,response.OfferingFiles);
                    ProcessSearchResponseReceived(this,new SearchResultReceivedEventArgs(results));                    
                    return; //we do not need to forward search responses which belong to one of our searches
                }

                //this is not a result for us, so we forward it
                //if the hopcount > 0 we continue flooding
                if (response.Hopcount > 0)
                {
                    //flood to everyone except the one who sent us the message
                    var connections = from Connection c in Neighbours
                                      where !c.RemoteAddress.Equals(conn.RemoteAddress) ||
                                            !c.RemotePort.Equals(conn.RemotePort)
                                      select c;

                    var payload = Serialisation.Serialize(response, typeof(SearchResponseMessage));
                    IHeader header = new Header(MessageTypes.SEARCH_RESPONSE, payload.Length);
                    IMessage message = new Message(header, payload);

                    foreach (var con in connections)
                    {

                        try
                        {
                            con.SendMessage(message);
                            this.Logger.Debug(String.Format("Forward response message for {0} to {1}:{2}", response.Regex, con.RemoteAddress, con.RemotePort));
                        }
                        catch (Exception ex)
                        {
                            this.Logger.Error(String.Format("Could not forward response message for {0} to {1}:{2}: {3}", response.Regex, con.RemoteAddress, con.RemotePort, ex.Message));
                        }
                    }
                }
            }
        }

        #endregion


        #region Search

        private static byte[] CalculateHash(string regEx, string now, string ip, string port)
        {
            //Generating HASH with SHA256
            var shaM = new SHA256Managed();
            var b = new List<byte>();
            var enc = new ASCIIEncoding();
            b.AddRange(enc.GetBytes(regEx));
            b.AddRange(enc.GetBytes(now));
            b.AddRange(enc.GetBytes(ip));
            b.AddRange(enc.GetBytes(port));
            var hash = shaM.ComputeHash(b.ToArray());
            return hash;
        }

        private static bool EqualArrays(byte[] array1, byte[] array2)
        {
            if (array1 != null && array2 != null && array1.Length == array2.Length)
            {
                for (int i = 0; i < array1.Length; i++)
                {
                    if (!array1[i].Equals(array2[i]))
                    {
                        return false;
                    }
                }
            }
            return true;
        }

        private static void CleanLists()
        {
            if (_AlreadySeenSearchRequests.Count > 100)
            {
                _AlreadySeenSearchRequests.RemoveRange(0, 90);
            }
            if (_WeSearchFor.Count > 100)
            {
                _WeSearchFor.RemoveRange(0, 90);
            }
            if (_SeenMessages.Count > 100)
            {
                _SeenMessages.RemoveRange(0, 90);
            }
        }

        private void ProcessSearchResponseReceived(object sender, SearchResultReceivedEventArgs e)
        {
            if (this.SearchResultReceived != null)
            {
                this.SearchResultReceived.Invoke(this, e);
            }
        }

        private void ProcessSearchRequest(object sender, SearchRequestReceivedEventArgs e)
        {
            if (this.SearchRequestReceived != null)
            {
                this.SearchRequestReceived.Invoke(this, e);
            }
        }


        #endregion


        #region Bootstrapping

        private void HandleBootstrapPeerTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            CheckRequestPeerList();
        }

        private void StartBootstrapPingTimer()
        {
            if (!this.bootstrapPingTimer.Enabled)
            {
                this.bootstrapPingTimer.Interval = this.BootstrapPingInverval * 1000;
                this.bootstrapPingTimer.Start();
            }
        }

        private void StopBootstrapPingTimer()
        {
            if (this.bootstrapPingTimer.Enabled)
            {
                this.bootstrapPingTimer.Stop();
            }
        }

        private void CheckRequestPeerList()
        {
            StopBootstrapPingTimer();
            // Checks whether new connections are neccessary to be made.
            try
            {
                if (this.overlayState == State.Shutdown)
                {
                    return;
                }

                int connectionsNeeded = this.MinConnection - this.Neighbours.Count;
                if (connectionsNeeded > 0)
                {
                    this.bsClient.RequestPeers(connectionsNeeded);
                }
                else
                {
                    this.bsClient.PingBootstrapServer();
                }
            }
            catch (NetworkException ex)
            {
                this.Logger.Error("Could not request peer list!", ex);
                if (this.BootstrapServerIsDown != null)
                {
                    this.BootstrapServerIsDown.Invoke(this, new EventArgs());
                }
            }
            StartBootstrapPingTimer();
        }

        private void HandlePeerListReceived(object sender, PeerListReceivedEventArgs e)
        {
            _myIp = e.List.IpAdress;
            foreach (PeerEntry peer in e.List.Peers)
            {
                // PeerEntryList is filled from above, so stop on the first null entry
                if (peer == null)
                {
                    break;
                }

                // Check if we are already connected to this peer.
                bool alreadyConnected = false;
                foreach (IConnection conn in this.Neighbours)
                {
                    if (conn.RemoteAddress.Equals(peer.IP) && conn.RemotePort.Equals(peer.Port))
                    {
                        alreadyConnected = true;
                        break;
                    }
                }
                if (alreadyConnected)
                {
                    continue;
                }

                // Maybe new connections were requested while we are talking to the bootstrap server... so stop creating new connections
                if (this.Neighbours.Count > this.MaxConnection)
                {
                    return;
                }

                // Connect
                try
                {
                    IConnection peerConnection = new Connection();
                    peerConnection.ConnectionClosed += new EventHandler<EventArgs>(HandlePeerConnectionClosed);
                    peerConnection.ConnectionTimedOut += new EventHandler<EventArgs>(HandlePeerConnectionTimedOut);
                    peerConnection.MessageReceived += new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);
                    peerConnection.ReadTimeout = this.Timeout;
                    peerConnection.WriteTimeout = this.Timeout;
                    peerConnection.Open(peer.IP, peer.Port);

                    this.Neighbours.Add(peerConnection);
                    this.Logger.Debug(String.Format("Successfully connected to {0}:{1}", peerConnection.RemoteAddress, peerConnection.RemotePort));
                }
                catch (Exception ex)
                {
                    this.Logger.Error(String.Format("Could not open connection to {0}:{1}", peer.IP, peer.Port), ex);
                }
            }
            if (this.overlayState == State.Connecting)
            {
                this.overlayState = State.Connected;
            }
            this.Logger.Debug(String.Format("Successfully connected to peer to peer network. Current neighbours: {0}", this.Neighbours.Count));
        }

        private void HandleBootstrapTimedOut(object sender, EventArgs e)
        {
            if (this.BootstrapServerIsDown != null)
            {
                this.BootstrapServerIsDown.Invoke(this, e);
            }
        }

        #endregion


        #region Peer connection management

        private void HandlePeerPingTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            StopPeerPingTimer();
            PingNeighbours();
            StartPeerPingTimer();
        }

        private void StartPeerPingTimer()
        {
            if (!this.peerPingTimer.Enabled)
            {
                this.peerPingTimer.Interval = this.PeerPingInverval * 1000;
                this.peerPingTimer.Start();
            }
        }

        private void StopPeerPingTimer()
        {
            if (this.peerPingTimer.Enabled)
            {
                this.peerPingTimer.Stop();
            }
        }

        private void CloseConnection(IConnection conn)
        {
            conn.ConnectionClosed -= new EventHandler<EventArgs>(HandlePeerConnectionClosed);
            conn.ConnectionTimedOut -= new EventHandler<EventArgs>(HandlePeerConnectionTimedOut);
            conn.MessageReceived -= new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);
            conn.Close();
        }

        private void StartListen(object o)
        {
            IConnection conn = (IConnection)o;
            try
            {
                StartPeerPingTimer();
                conn.NewConnectionEstablished += new EventHandler<ConnectionEventArgs>(HandleNewPeerConnection);
                conn.Listen(System.Net.IPAddress.Parse(this.ListenAddress), this.ListenPort, (uint)this.MaxConnection);
            }
            catch (Exception ex)
            {
                this.Logger.Error("Error while listening on peer connection!", ex);
            }
        }

        private void PingNeighbours()
        {
            Ping ping = new Ping();
            byte[] payload = Serialisation.Serialize(ping, typeof(Ping));
            IHeader header = new Header(MessageTypes.PING, payload.Length);
            IMessage message = new Message(header, payload);

            StringBuilder sb = new StringBuilder();
            sb.Append("Send Ping message to neighbours ( ");

            foreach (IConnection conn in this.Neighbours)
            {
                try
                {
                    conn.SendMessage(message);
                    sb.Append(conn.RemoteAddress).Append(":").Append(conn.RemotePort).Append(" ");
                }
                catch (Exception ex)
                {
                    this.Logger.Warn(String.Format("Error while pinging peer {0}:{1}!", conn.RemoteAddress, conn.RemotePort), ex);
                }
            }
            sb.Append(")");
            this.Logger.Debug(sb.ToString());
        }

        private void HandleNewPeerConnection(object sender, ConnectionEventArgs e)
        {
            e.Connection.ConnectionClosed += new EventHandler<EventArgs>(HandlePeerConnectionClosed);
            e.Connection.ConnectionTimedOut += new EventHandler<EventArgs>(HandlePeerConnectionTimedOut);
            e.Connection.MessageReceived += new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);

            this.Neighbours.Add(e.Connection);
            this.Logger.Debug(String.Format("Peer {0}:{1} has connected! Local socket: {2}:{3} | Active connections {4}", e.Connection.RemoteAddress, e.Connection.RemotePort, e.Connection.LocalAddress, e.Connection.LocalPort, this.Neighbours.Count));
        }

        private void HandlePeerConnectionClosed(object sender, EventArgs e)
        {
            IConnection conn = (IConnection)sender;
            CloseConnection(conn);
            this.Neighbours.Remove(conn);
            this.Logger.Debug(String.Format("Connection to peer {0}:{1} has been closed! | Active connections {2}", conn.RemoteAddress, conn.RemotePort, this.Neighbours.Count));
        }

        private void HandlePeerConnectionTimedOut(object sender, EventArgs e)
        {
            IConnection conn = (IConnection)sender;
            CloseConnection(conn);
            this.Neighbours.Remove(conn);
            this.Logger.Debug(String.Format("Connection to peer {0}:{1} has been timed out! | Active connections {2}", conn.RemoteAddress, conn.RemotePort, this.Neighbours.Count));
        }

        #endregion 


        #region Properties and private member

        /// <summary>
        /// The IP address or hostname of the bootstrap server.
        /// </summary>
        public string BootstrapServerAddress { get; private set; }

        /// <summary>
        /// The port number of the bootstrap server.
        /// </summary>
        public int BootstrapServerPort { get; private set; }

        /// <summary>
        /// The IP address used to listen for new connections.
        /// </summary>
        public string ListenAddress { get; private set; }

        public string MyIp
        {
            get { return _myIp; }
        }

        /// <summary>
        /// The port number used to listen for new connections.
        /// </summary>
        public int ListenPort { get; private set; }

        /// <summary>
        /// Sending and receiving messages will timeout after the specified time in seconds.
        /// </summary>
        public int Timeout { get; set; }

        /// <summary>
        /// Lower bound of active peer connections. 
        /// If the peer drops below this value he asks the bootstrap server for new peers
        /// </summary>
        public int MinConnection { get; set; }

        /// <summary>
        /// Upper bound of active peer connections. 
        /// If the peer reaches this number of active peer connections, new connection attempts will be rejected.
        /// </summary>
        public int MaxConnection { get; set; }

        /// <summary>
        /// The maximum number of hops, a message will be flooded.
        /// </summary>
        public int MaxHopCount { get; set; }

        /// <summary>
        /// A list of peers, this peer is currently connected to.
        /// </summary>
        public ThreadSafeList<IConnection> Neighbours { get; private set; }

        /// <summary>
        /// The logger used to log messages.
        /// </summary>
        public ILogger Logger { get; private set; }

        /// <summary>
        /// Time in seconds, the peer will ping the bootstrap server.
        /// </summary>
        public int BootstrapPingInverval { get; set; }

        /// <summary>
        /// Time in seconds, the peer will ping other peers.
        /// </summary>
        public int PeerPingInverval { get; set; }

        private State overlayState;

        private IBootstrapClient bsClient;
        
        private IConnection listenConnection;

        private System.Timers.Timer bootstrapPingTimer;

        private System.Timers.Timer peerPingTimer;

        private static ArrayList _AlreadySeenSearchRequests = ArrayList.Synchronized(new ArrayList());

        private static ArrayList _WeSearchFor = ArrayList.Synchronized(new ArrayList());

        private static ArrayList _SeenMessages = ArrayList.Synchronized(new ArrayList());

        private Thread listenThread;
        
        private string _myIp;

        #endregion


        #region Events

        /// <summary>
        /// Could not connect because the bootstrap server is not available.
        /// </summary>
        public event EventHandler<EventArgs> BootstrapServerIsDown;

        /// <summary>
        /// The result of a file search has been received.
        /// </summary>
        public event EventHandler<SearchResultReceivedEventArgs> SearchResultReceived;

        /// <summary>
        /// Received a file search from another peer.
        /// </summary>
        public event EventHandler<SearchRequestReceivedEventArgs> SearchRequestReceived;

        #endregion

    }
}
